package com.yvan.idempotent;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yvan.springmvc.JsonView;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * Created by Administrator on 2017/7/28.
 */
@Aspect
@Component
public class IdempotentAspect {

    private static final long DEFAULT_MAX_WAIT = 10 * 60;
    private static final String TOKEN_KEY = "token";
    private static final String IDEMPOTENT_FIRST_KEY = "Idempotent";
    private static final String STATUS_DOING = "doing";
    private Logger LOGGER = LoggerFactory.getLogger(IdempotentAspect.class);
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Around("@annotation(idempotent)")
    public Object executeIdempotentCall(ProceedingJoinPoint pjp, Idempotent idempotent) throws Throwable {
        LOGGER.info("Signature:" + pjp.getSignature());
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

        long expire = idempotent.expireSeconds() == -1 ? DEFAULT_MAX_WAIT : idempotent.expireSeconds();

        IdempotentRequestWrapper request = findIdempotentRequestArg(pjp);
        String token = findTokenKey(request);
        if (stringRedisTemplate.opsForValue().get(token) != null) {

            String resultStr = stringRedisTemplate.opsForValue().get(token);
            if (resultStr.equals(STATUS_DOING)) {
                ModelAndView modelAndView;
                Map map = new HashMap<>();
                map.put("success", false);
                if (idempotent.redirect().length() > 0) {
                    map.put("message", "订单重复提交！");
                    modelAndView = new ModelAndView(new JsonView(map));
                } else {
                    //TODO fix with common doing ftl
                    map.put("message", "订单重复提交！");
                    modelAndView = new ModelAndView(new JsonView(map));
                }
                return modelAndView;
            }

            Class returnType = ((MethodSignature) pjp.getSignature()).getReturnType();
            Object obj = objectMapper.readValue(resultStr, returnType);
            return obj;
        }

        //doing
        stringRedisTemplate.opsForValue().set(token, STATUS_DOING, expire, TimeUnit.SECONDS);
        Object result = null;
        try {
            result = pjp.proceed();
        } catch (Throwable ex) {
            LOGGER.error("pjp in idempotent has been error:" + ex.getMessage(), ex);
            stringRedisTemplate.delete(token);
            throw ex;
        }

        //done
        if (idempotent.completeClean()) {
            stringRedisTemplate.delete(token);
        } else {
            stringRedisTemplate.opsForValue().set(token, objectMapper.writeValueAsString(result), expire, TimeUnit.SECONDS);
        }
        return result;
    }

    private String findTokenKey(IdempotentRequestWrapper idempotentRequest) {
        String token = "";
        Object req = idempotentRequest.getRequest();

        if (req instanceof HttpServletRequest) {
            //TODO get Token from http head > param > post data (if need ?)
            token = ((HttpServletRequest) req).getHeader(TOKEN_KEY);
            if (token == null) {
                token = ((HttpServletRequest) req).getParameter(TOKEN_KEY);
            }
            if (token == null) {
                //post data?
            }
        } else if (req instanceof String) {
            token = req.toString();
        }
        return IDEMPOTENT_FIRST_KEY + ":" + token;
    }


    private IdempotentRequestWrapper findIdempotentRequestArg(ProceedingJoinPoint pjp) {
        Object[] args = pjp.getArgs();
        if (args.length == 0) {
            return new IdempotentRequestWrapper(null);
        } else {
            try {
                MethodSignature signature = (MethodSignature) pjp.getSignature();
                String methodName = signature.getMethod().getName();
                Class<?>[] parameterTypes = signature.getMethod().getParameterTypes();
                Annotation[][] annotations = pjp.getTarget().getClass().getMethod(methodName, parameterTypes)
                        .getParameterAnnotations();
                for (int i = 0; i < args.length; i++) {
                    for (Annotation annotation : annotations[i]) {
                        if (annotation instanceof IdempotentRequest) {
                            return new IdempotentRequestWrapper(args[i]);
                        }
                    }
                }
            } catch (NoSuchMethodException | SecurityException e) {
                throw new IllegalStateException("Idempotent method not found", e);
            }
        }
        return null;
    }
}
